package scenic;

import java.awt.Rectangle;
import java.util.Vector;

import scenic.geom.Affine2;
import scenic.jni.Renderer;

/**
 * This scene node uses double buffering to reduce rendering time.
 * This class also supports fast scrolling.
 * 
 */
public class SceneDoubleBuffer extends SceneContainer {
	private class Buffer
	{
		public Image buffer;
		public Affine2 bufferTransform;
		public Rectangle bufferVisibleArea;

		private void updateBuffer(Image oldBuffer, DrawContext context, Affine2 newTransform, Rectangle newVisibleArea,
				Rectangle oldVisibleArea) {
			buffer = new Image(bufferVisibleArea.width, bufferVisibleArea.height, 
					Format.A8R8G8B8, ImageOptions.RENDER_TARGET);
			context.context = context.renderer.createContextForImage(buffer.getId());				
			context.renderer.beginScene(context.context);
			if(oldBuffer != null) {
				int dx = oldVisibleArea.x - bufferVisibleArea.x;
				int dy = oldVisibleArea.y - bufferVisibleArea.y;
				
				context.renderer.color(context.context, 0, 0, 0, 0);
				context.renderer.clear(context.context);
				
				if(dy > 0) {
					Rectangle r = new Rectangle(0, 0, buffer.getWidth(), dy);
					
					if(context.renderer.beginClip(context.context, r)) {
						context.renderer.color(context.context, 0, 0, 0, 0);
						context.renderer.clear(context.context);
						SceneDoubleBuffer.super.draw(context, newTransform, r);
						context.renderer.discardSurface(context.context);
					}
				}
				if(dy + oldBuffer.getHeight() < buffer.getHeight()) {
					Rectangle r = new Rectangle(0, dy + oldBuffer.getHeight(), buffer.getWidth(), 
							buffer.getHeight() - (dy + oldBuffer.getHeight()));
					
					if(context.renderer.beginClip(context.context, r)) {
						context.renderer.color(context.context, 0, 0, 0, 0);
						context.renderer.clear(context.context);
						SceneDoubleBuffer.super.draw(context, newTransform, r);
						context.renderer.discardSurface(context.context);
					}
				}
				if(dx > 0) {
					Rectangle r = new Rectangle(0, 0, dx, buffer.getHeight());
					
					if(context.renderer.beginClip(context.context, r)) {
						context.renderer.color(context.context, 0, 0, 0, 0);
						context.renderer.clear(context.context);
						SceneDoubleBuffer.super.draw(context, newTransform, r);
						context.renderer.discardSurface(context.context);
					}
				}
				if(dx + oldBuffer.getWidth() < buffer.getWidth()) {
					Rectangle r = new Rectangle(dx + oldBuffer.getWidth(), 0, 
							buffer.getWidth() - (dx + oldBuffer.getWidth()), buffer.getHeight());
					
					if(context.renderer.beginClip(context.context, r)) {
						context.renderer.color(context.context, 0, 0, 0, 0);
						context.renderer.clear(context.context);
						SceneDoubleBuffer.super.draw(context, newTransform, r);
						context.renderer.discardSurface(context.context);
					}
				}
				context.renderer.setTransform(context.context, 1.0, 0.0, 0.0, 1.0, dx, dy);
				context.renderer.color(context.context, 1, 1, 1, 1);
				context.renderer.drawImage(context.context, oldBuffer.getId());
				oldBuffer.free();
			} else {
				context.renderer.color(context.context, 0, 0, 0, 0);
				context.renderer.clear(context.context);
				SceneDoubleBuffer.super.draw(context, newTransform, newVisibleArea);
			}
			context.renderer.endScene(context.context);
			context.renderer.deleteContext(context.context);
		}
		
		void draw(DrawContext context, Affine2 transform, Rectangle visibleArea) {
			boolean validBuffer = buffer != null && !buffer.contentsLost() && !repaintBuffer;
			boolean useOldBuffer = false;
			
			if(validBuffer) {
				int dx = (int)Math.round(transform.dx - bufferTransform.dx);
				int dy = (int)Math.round(transform.dy - bufferTransform.dy);
				
				bufferVisibleArea.translate(dx, dy);
				bufferTransform.dx = transform.dx;
				bufferTransform.dy = transform.dy;
				if(!equalsLinear(bufferTransform, transform) ||
						!compatibleOffset(bufferTransform, transform) ||
						!bufferVisibleArea.contains(visibleArea)) {
					if(equalsLinear(bufferTransform, transform) ||
							!compatibleOffset(bufferTransform, transform)) {
						useOldBuffer = true;
					}
					validBuffer = false;
				}
			}

			if(!validBuffer) {
				Image oldBuffer = null;
				Rectangle oldVisibleArea = bufferVisibleArea;
				
				if(buffer != null) {
					if(useOldBuffer)
						oldBuffer = buffer;
					else
						buffer.free();
				}
				
				if(useOldBuffer) {
					bufferVisibleArea = (Rectangle)bufferVisibleArea.clone();			
					bufferVisibleArea.width = visibleArea.width + margin;
					bufferVisibleArea.height = visibleArea.height + margin;
					if(bufferVisibleArea.x > visibleArea.x)
						bufferVisibleArea.x = visibleArea.x - margin;
					else if(bufferVisibleArea.getMaxX() < visibleArea.getMaxX())
						bufferVisibleArea.x = visibleArea.x;
					if(bufferVisibleArea.y > visibleArea.y)
						bufferVisibleArea.y = visibleArea.y - margin;
					else if(bufferVisibleArea.getMaxY() < visibleArea.getMaxY())
						bufferVisibleArea.y = visibleArea.y;
				} else {
					bufferVisibleArea = (Rectangle)visibleArea.clone();			
					bufferVisibleArea.x -= margin / 2;
					bufferVisibleArea.y -= margin / 2;
					bufferVisibleArea.width += margin;
					bufferVisibleArea.height += margin;				
				}
				
				DrawContext newContext = new DrawContext(context, Renderer.SURFACE_TYPE_COLOR);
				Affine2 newTransform = (Affine2)transform.clone();
				Rectangle newVisibleArea = new Rectangle(0, 0, bufferVisibleArea.width, bufferVisibleArea.height);
				
				bufferTransform = transform;
				newTransform.dx -= bufferVisibleArea.x;
				newTransform.dy -= bufferVisibleArea.y;
				
				updateBuffer(oldBuffer, newContext, newTransform, newVisibleArea, oldVisibleArea);
//				System.out.println("create");
			}
			context.renderer.setTransform(context.context, 1.0, 0.0, 0.0, 1.0, bufferVisibleArea.x, 
					bufferVisibleArea.y);
			context.renderer.color(context.context, 1, 1, 1, 1);
			context.renderer.drawImage(context.context, buffer.getId());
		}
		
		void free() {
			if(buffer != null) {
				buffer.free();
				buffer = null;
			}
		}
	}

	private static final int margin = 256;
	private Vector<Buffer> buffers = new Vector<Buffer>();
	private boolean repaintBuffer;
	private boolean repaintNextBuffer;
	private Object renderingInstance;
	private int count;
	
	/**
	 * Constructs an empty SceneDoubleBuffer object.
	 */
	public SceneDoubleBuffer() {
	}
	
	/**
	 * Constructs a SceneDoubleBuffer object with the initial content.
	 
	 * @param content Initial content.
	 */
	public SceneDoubleBuffer(SceneNode content) {
		add(content);
	}
	
	void draw(DrawContext context, Affine2 transform, Rectangle visibleArea) {
		if(context.renderingInstance != renderingInstance) {
			while(buffers.size() > count) {
				buffers.get(count).free();
				buffers.remove(count);
			}
			renderingInstance = context.renderingInstance; 
			count = 0;
			repaintBuffer = repaintNextBuffer;
			repaintNextBuffer = false;
		}
		if(buffers.size() < count + 1) {
			buffers.add(count, new Buffer());
		}
		buffers.get(count).draw(context, transform, visibleArea);
		count++;
	}
	
	protected void hide() {
		super.hide();
		for(Buffer n : buffers) {
			n.free();
		}
		buffers.clear();
		renderingInstance = null;
		count = 0;
	}
	
	public void childChanged(Rectangle area) {
		super.childChanged(area);
		repaintNextBuffer = true;
	}
	
	private static boolean compatibleOffset(double a, double b) {
		double d = a - b;
		
		return Math.abs(Math.floor(d + 0.5) - d) < 0.0001;
	}
	
/*	private static boolean equals(Affine2 a, Affine2 b) {
		return a.m00 == b.m00 &&
			a.m01 == b.m01 &&
			a.m10 == b.m10 &&
			a.m11 == b.m11 &&
			a.dx == b.dx &&
			a.dy == b.dy;
	} */
	
	private static boolean equalsLinear(Affine2 a, Affine2 b) {
		return a.m00 == b.m00 &&
			a.m01 == b.m01 &&
			a.m10 == b.m10 &&
			a.m11 == b.m11;
	}
	
/*	private static boolean equalsOffset(Affine2 a, Affine2 b) {
		return a.dx == b.dx &&
			a.dy == b.dy;
	} */
	
	private static boolean compatibleOffset(Affine2 a, Affine2 b) {
		return compatibleOffset(a.dx, b.dx) &&
			compatibleOffset(a.dy, b.dy);
	}
}
